import { spawn } from 'child_process'
import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from 'child_process'
import iconv from 'iconv-lite'
import stripAnsi from 'strip-ansi'
import splitLines from 'split-lines'
import { CommonUtil } from '@xtail/core'
import { ResultType, ResultUtil } from '@xtail/result'
import { NodeUtil } from '@xtail/node'

export const Shell_Name = 'Shell'

export class Shell {
  // 与本机建立终端长会话的配置
  private readonly config: SpawnOptionsWithoutStdio
  // 本机操作系统平台的 Shell，默认 windows 使用 cmd.exe，其他平台使用 /bin/bash
  private readonly platformShell: string
  // 与本机建立终端长会话的进程实例
  private shell: ChildProcessWithoutNullStreams | null = null
  // 当前执行的命令
  private currentCmd: Record<string, string> = {}

  /**
   * 创建 Shell 实例
   *
   * @param {SpawnOptionsWithoutStdio} config 与本机建立终端长会话的配置，默认配置：
   * - cwd：Shell 会话中当前工作目录，默认为当前程序运行所在的目录
   * - shell：是否在 Shell 中执行命令，默认为 true
   * @param {string} platformShell 本机操作系统平台的 Shell，默认 windows 使用 cmd.exe，
   * 其他平台使用 /bin/bash
   */
  constructor(
    config: SpawnOptionsWithoutStdio = {},
    platformShell: string = NodeUtil.getSysShell()
  ) {
    this.config = {
      cwd: process.cwd(),
      shell: true,
      ...config
    }
    this.platformShell = platformShell
  }

  /**
   * 是否已经与本机建立终端长会话
   *
   * @returns {boolean} true 本机终端会话已建立，false 本机终端会话未建立
   */
  get isConnected(): boolean {
    return !CommonUtil.isNull(this.shell) && !this.shell?.killed
  }

  /**
   * 与本机建立终端长会话<br/>
   * 需要注意：
   * - 与本机建立终端长会话，是通过 spawn 方法创建一个<b style="color:red">子进程</b>实现的
   *
   * @param {(data: string) => void} onData 监听终端数据输出的回调函数，参数：终端输出的
   * 数据，默认值 () => {}
   * @param {(error: string) => void} onError 监听终端错误数据输出的回调函数，参数：终端
   * 输出的错误数据，默认值 () => {}
   * @param {(code: (number | null), signal: (NodeJS.Signals | null)) => void} onClose
   * 监听与本机终端建立的长会话关闭的回调函数，参数：
   * - code 如果子进程自行退出，则向该回调函数传入退出代码，否则为 null
   * - signal 如果子进程被信号终止，则向该回调函数传入信号，否则为 null
   * 默认值 () => {}
   * @returns {Promise<ResultType>} 与本机建立终端长会话的结果
   */
  connect(
    onData: (data: string) => void = () => {},
    onError: (error: string) => void = () => {},
    onClose: (code: number | null, signal: NodeJS.Signals | null) => void = () => {}
  ): ResultType {
    try {
      // 如果已经与本机建立了终端长会话，则先关闭之前的会话
      if (this.isConnected) return ResultUtil.genSuccessResult('本机终端会话已建立')
      // 创建与本机进行终端长会话的进程实例
      this.shell = spawn(this.platformShell, [], this.config)
      // 获取当前操作系统的编码格式
      const encoding = NodeUtil.getSysEncodeFormat()
      // 排除与当前执行的命令相同的数据
      const excludeCmd = (data: Buffer) => {
        // 最终要返回的数据
        let dataStr = ''
        // 由于服务器可能会一次性返回多行数据，所以分行判断排除与当前执行的命令相同的数据
        const dataLines = splitLines(iconv.decode(data, encoding))
        for (const dataLine of dataLines) {
          // 如果返回的数据为当前正在执行的命令，则不处理本次服务器返回的数据
          if (Reflect.has(this.currentCmd, dataLine.trim())) {
            Reflect.deleteProperty(this.currentCmd, dataLine.trim())
          } else if (Reflect.has(this.currentCmd, stripAnsi(dataLine).trim())) {
            Reflect.deleteProperty(this.currentCmd, stripAnsi(dataLine).trim())
          } else {
            dataStr += dataLine + '\n'
          }
        }
        return dataStr
      }
      // 监听终端数据输出
      this.shell.stdout.on('data', (data: Buffer) => {
        const dataStr = excludeCmd(data)
        // 如果最终要返回的数据为空，直接退出函数
        if (!dataStr.trim()) return
        onData(dataStr)
      })
      // 监听终端错误输出
      this.shell.stderr.on('data', (data: Buffer) => {
        const dataStr = excludeCmd(data)
        // 如果最终要返回的数据为空，直接退出函数
        if (!dataStr.trim()) return
        onError(dataStr)
      })
      // 监听终端关闭事件
      this.shell.on('close', (code, signal) => {
        onClose(code, signal)
      })
      return ResultUtil.genSuccessResult('本机终端会话建立成功')
    } catch (e: any) {
      return ResultUtil.genFailResult(e?.message ?? '本机终端会话建立失败', e)
    }
  }

  /**
   * 在与本机建立的终端长会话中执行命令
   *
   * @param {string} cmd 需要执行的命令
   * @param {() => void} drain 终端内部缓冲区到达阈值，等待 drain 事件触发后，执行该回调函数
   * @returns {Promise<ResultType>} 执行命令的结果，结果对象的 data 字段值为 true 表示可以继续向终端中写入命令，false 表示终端内部缓冲
   * 区已达到阈值，需要暂停写入命令
   */
  async exec(cmd: string, drain: () => void = () => {}): Promise<ResultType> {
    if (!this.isConnected) return ResultUtil.genFailResult('本机终端会话未建立')
    return await new Promise(resolve => {
      this.currentCmd[cmd] = cmd
      // 写入命令到终端
      const res = this.shell!.stdin.write(cmd + '\n', (err: any) => {
        resolve(ResultUtil.genFailResult(err?.message ?? `${cmd} 执行失败`, err))
      })
      // 执行到此，命令写入终端成功
      if (!res) {
        // 终端内部缓冲区到达阈值，等待 drain 事件后，才可继续写入命令
        this.shell!.stdin.once('drain', () => drain())
        resolve(ResultUtil.genSuccessResult(`${cmd} 执行成功`, false))
      } else {
        // 终端内部缓冲区未到达阈值
        resolve(ResultUtil.genSuccessResult(`${cmd} 执行成功`, true))
      }
    })
  }

  /**
   * 关闭与本机终端的长会话
   *
   * @returns {ResultType} 关闭与本机终端的长会话的结果
   */
  close(): ResultType {
    if (!this.shell?.killed) {
      try {
        this.shell?.kill()
        this.shell = null
        this.currentCmd = {}
        return ResultUtil.genSuccessResult('本机终端会话关闭成功')
      } catch (e: any) {
        return ResultUtil.genFailResult(e?.message ?? '本机终端会话关闭失败', e)
      }
    } else {
      this.shell = null
      this.currentCmd = {}
      return ResultUtil.genSuccessResult('本机终端会话已关闭')
    }
  }
}
